The ImaGen package provides comprehensive support for creating resolution-independent one- and two-dimensional pattern distributions. ImaGen consists of a large library of primarily two-dimensional spatial patterns, including mathematical functions, geometric primitives, and images read from files, along with many ways to combine or select from any other patterns. These patterns can be used in any Python program that needs configurable patterns or streams of patterns. Basically, as long as the code can accept a Python callable and will call it each time it needs a new pattern, users can then specify any pattern possible in ImaGen's simple declarative pattern language, and the downstream code need not worry about any of the details about how the pattern is specified or generated. This approach gives users full flexibility about which patterns they wish to use, while relieving the downstream code from having to implement anything about patterns. See below for more background.
Some example patterns:

To create a pattern, just import imagen, then instantiate one of ImaGen's PatternGenerator classes. Each of these classes support various parameters, which are each described in the Reference Manual or via help(pattern-object-or-class). Any parameter values specified on instantiation become the defaults for that object:
import imagen as ig
line=ig.Line(xdensity=5, ydensity=5, smoothing=0)
Then whenever the line object is called, you'll get a new NumPy array:
line()
Here the parameters xdensity and ydensity specified that a continuous 1.0×1.0 region in (x,y) space should be sampled on a 5×5 grid. The line object can now be called repeatedly, with any parameter values specified to override those declared above:
import numpy as np
np.set_printoptions(1)
line(smoothing=0.1,orientation=0.8,thickness=0.4)
ImaGen depends only on NumPy, Param, and HoloViews, none of which have any other required dependencies, and it is thus easy to incorporate ImaGen into your own code to generate or use patterns freely. An optional interface to matplotlib via HoloViews is also available, which provides a convenient way to plot the pattern objects:
import holoviews
%load_ext holoviews.ipython
line.set_param(xdensity=72,ydensity=72,orientation=np.pi/4, thickness=0.1, smoothing=0.02)
line[:]
We will use this plotting interface to show off the remaining patterns, but please remember that the main purpose of ImaGen is to generate arrays for use in other programs, not simply to draw patterns for plotting!
As you can see above, PatternGenerator objects return different patterns depending on their parameter values. An important feature of these parameter values is that any of them can be set to "dynamic" values, which will then result in a different pattern each time (see the Param package and its numbergen module for details). Here, let's define a SineGrating object with a random orientation, collect four of them at different times (using the .anim() method), and lay them out next to each other (using the NdLayout class from HoloViews):
import numbergen as ng
from holoviews import NdLayout
import param
param.Dynamic.time_dependent=True
NdLayout(ig.SineGrating(orientation=np.pi*ng.UniformRandom()).anim(3))
As you can see, each time the sine grating was rendered, the pattern differed, because the parameter value for orientation was chosen randomly. Of course, you can set any combination of patterns to dynamic values, to get arbitrarily complex variation over time:
%%opts Image (cmap='gray')
sine_disk = ig.SineGrating(orientation=np.pi*ng.UniformRandom(),
scale=0.25*ng.ExponentialDecay(time_constant=3),
frequency=4+7*ng.UniformRandom(),
x=0.3*ng.NormalRandom(seed=1),
y=0.2*ng.UniformRandom(seed=2)-0.1,
mask_shape=ig.Disk(size=0.5,smoothing=0.01))
NdLayout(sine_disk.anim(3))
As you can see, PatternGenerator objects can also be used as a mask for another PatternGenerator, which is one simple way to combine them.
PatternGenerators can also be combined directly with each other to create Composite PatternGenerators, which can make any possible 2D pattern. For instance, we can easily sum 10 oriented Gaussian patterns, each with random positions and orientations, giving a different overall pattern at each time:
gs = ig.Composite(operator=np.add,
generators=[ig.Gaussian(size=0.15,
x=ng.UniformRandom(seed=i+1)-0.5,
y=ng.UniformRandom(seed=i+2)-0.5,
orientation=np.pi*ng.UniformRandom(seed=i+3))
for i in range(10)])
NdLayout(gs.anim(4)).cols(5)
A Composite pattern works just like any other pattern, so that it can be placed, rotated, combined with others, etc., allowing you to build up arbitrarily complex objects out of simple primitives. Here we created a Composite pattern explicitly, but it's usually easier to create them by simply using any of the usual Python operators (+, -, *, /, **, %, & (min), and | (max)) as in the examples below (syntax requires latest git version).
For instance, here's an example using np.maximum (via the | operator on PatternGenerators), rotating the composite pattern together as a unit. We also leave it as a HoloViews animation rather than laying it out over space:
%%opts Image.Pattern (cmap='Blues_r')
l1 = ig.Line(orientation=-np.pi/4)
l2 = ig.Line(orientation=+np.pi/4)
cross = l1 | l2
cross.orientation=ng.ScaledTime()*(np.pi/-20)
l1.anim(20) + l2.anim(20) + cross.anim(20)
The .anim() method collects results at different times conveniently. What it's doing is repeatedly getting a copy of each pattern, then running param.Dynamic.time_fn.advance(1.0) to advance the nominal time, then getting another copy of each pattern until 20 different times have been sampled. The values are "time dependent" (because we set them to be so above), so that any randomness changes only when the time changes, and the randomness is computed as a function of time. That way, regardless of the order you generate the patterns, or even if you go back and forward in time, you will always get the same results at a given nominal time. In your own code, you can turn off time dependence (param.Dynamic.time_dependent=False), in which case new parameter values will be generated for every call to the PatternGenerator. Or, if you are working in a domain like simulation, you can set param.Dynamic.time_fn to a function based on your own nominal time, advancing it as appropriate. You can even set that function to real time, in which case you'll get completely unpredictable randomness, which may be appropriate in some circumstances. Whenever there is some notion of time that governs the patterns you want to see, setting time_dependent=True is a good idea, so that you have precise control over the randomness to ensure reproducible results.
We used one operator above to make the cross image, but we can combine operators in any combination, here to mask a sweeping Line pattern with a Disk, add the result to a rotating image, and create an animated GIF of the results with HoloViews:
%opts Image (cmap='gray')
import param
param.Dynamic.time_fn.advance(1)
print("The current nominal time value is %s" % param.Dynamic.time_fn())
%%output holomap='gif'
from imagen.image import FileImage
image = FileImage(size=0.5, scale=3, orientation=ng.ScaledTime()*np.pi/20)
line = ig.Line(y=0.6-ng.ScaledTime()*0.03)
disk = ig.Disk(smoothing=0.01, size=0.8)
(image + (line*disk)).anim(39)
Once the pattern has been generated, but before it is returned, you can apply any function to the data that you like, via the output_fns parameter. A variety of useful TransferFns are supplied for use as output_fns, such as thresholding functions, normalizing functions (L0, L1, L2, L-infinity, etc.), and convolutions. Any number of these or your own functions can be applied, in order:
import imagen.transferfn as tf
from imagen.transferfn.sheet_tf import Convolve
(FileImage()[:] + \
FileImage(output_fns=[tf.BinaryThreshold()])[:] + \
FileImage(output_fns=[Convolve()])[:] + \
FileImage(output_fns=[tf.BinaryThreshold(),Convolve()])[:] + \
FileImage(output_fns=[Convolve(kernel_pattern=ig.Gaussian(size=0.01)),tf.BinaryThreshold()])[:]).cols(5)
The above examples all show "single-channel" PatternGenerator objects, which are very general and usable for a huge variety of applications, as they are simply Numpy arrays.
PatternGenerator objects can have any number of channels (with each channel generating a Numpy array of the same size). Multi-channel patterns are used less often, but are particularly useful for generating color images. Color images loaded by the FileImage pattern will have four channels, one for the monochrome image (as above), and the other three for the red, green, and blue channels (accessed using object.channels()). RGB images can also be constructed by colorizing a monochrome pattern, or out of combinations of any of the other patterns, using the ComposeChannels object:
from imagen.image import ScaleChannels
ig.ComposeChannels(generators=[ig.Spiral(smoothing=0.02),ig.Spiral(),ig.Spiral(scale=0)])[:] + \
ig.ComposeChannels(generators=[ig.Line(orientation=np.pi/2),ig.Ring(),ig.SquareGrating()])[:]
Below are shown examples of each of the pattern types currently provided, using their default parameter values. Very many different parameter values can be chosen, to produce a much wider range of patterns, and adding a new pattern is very easy. One important pattern not yet covered in this tutorial is
Selector; which chooses from a set of patterns, e.g. an image selected randomly or sequentially from a database and then randomly rotated and translated depending on the parameters.
%opts Layout [sublabel_format="" horizontal_spacing=.1 vertical_spacing=.1]
%opts Image (cmap='gray') [show_xaxis=None show_yaxis=None show_frame=True]
from imagen import *
from imagen.random import *
from imagen.image import *
np.sum([x()[:] for _, x in sorted(locals().items()) if isinstance(x,type)
and issubclass(x,PatternGenerator) and not x.abstract]).cols(5)
ImaGen requires NumPy (http://numpy.scipy.org/), Param (http://ioam.github.com/param/), and HoloViews (http://ioam.github.com/holoviews/), none of which have any required external dependencies.
Official releases of ImaGen are available at http://pypi.python.org/pypi/imagen, and can be installed using pip. If you don’t have pip already, we recommend installing a scientific Python distribution like Anaconda first. Then installation of ImaGen and dependencies is simply:
pip install imagen
Questions and comments are welcome at https://github.com/ioam/imagen/issues.